package com.epam.rd.mycollections;

import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;

public class MyHashMap<K, V> implements MyMap<K, V> {

    private static final int INITIAL_CAPACITY = 16;
    private static final float LOAD_FACTOR = 0.75f;

    private SimpleEntry<K, V>[] table;
    private int size;

    private int initialCapacity;
    private float loadFactor;

    public MyHashMap() {
        this.initialCapacity = INITIAL_CAPACITY;
        this.loadFactor = LOAD_FACTOR;
    }

    public MyHashMap(int initialCapacity) {
        if (initialCapacity <= 0) {
            throw new IllegalArgumentException();
        }
        this.initialCapacity = initialCapacity;
        this.loadFactor = LOAD_FACTOR;
    }

    public MyHashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0 || loadFactor <= 0) {
            throw new IllegalArgumentException();
        }
        this.initialCapacity = initialCapacity;
        this.loadFactor = loadFactor;
    }

    static final class SimpleEntry<K, V> implements Entry<K, V> {
        private final int hash;
        private final K key;
        private V value;
        private SimpleEntry<K, V> next;

        SimpleEntry(int hash, K key, V value, SimpleEntry<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }

        @Override
        public V setValue(V newValue) {
            V oldValue = this.value;
            this.value = newValue;
            return oldValue;
        }
    }

    @Override
    public void clear() {
        if (size > 0) {
            size = 0;
            for (int i = 0; i < table.length; i++) {
                table[i] = null;
            }
        }
    }

    @Override
    public boolean containsKey(K key) {
        SimpleEntry<K, V> entry = getEntry(key);
        return entry != null;
    }

    @Override
    public boolean containsValue(V value) {
        for (SimpleEntry<K, V> entry : table) {
            SimpleEntry<K, V> nextEntry = entry;
            while (nextEntry != null) {
                if (Objects.equals(nextEntry.getValue(), value)) {
                    return true;
                }
                nextEntry = nextEntry.next;
            }
        }
        return false;
    }

    private SimpleEntry<K, V> getEntry(K key) {
        int hash = Objects.hashCode(key);
        int index;
        SimpleEntry<K, V> entry;
        if (table != null) {
            index = (table.length - 1) & hash;
            entry = table[index];
            while (entry != null) {
                if (hash == entry.hash && Objects.equals(entry.key, key)) {
                    return entry;
                }
                entry = entry.next;
            }
        }
        return null;
    }

    @Override
    public V get(K key) {
        SimpleEntry<K, V> entry = getEntry(key);
        if (entry != null) {
            return entry.value;
        }
        return null;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public V put(K key, V value) {
        resize();
        int hash = Objects.hashCode(key);
        int index = (table.length - 1) & hash;
        size++;
        if (table[index] == null) {
            table[index] = new SimpleEntry<K, V>(hash, key, value, null);
        } else {
            SimpleEntry<K, V> entry = table[index];
            SimpleEntry<K, V> prev = null;
            do {
                if (entry.hash == hash && Objects.equals(key, entry.key)) {
                    return entry.setValue(value);
                }
                prev = entry;
                entry = entry.next;
            } while (entry != null);
            prev.next = new SimpleEntry<K, V>(hash, key, value, null);
        }
        return null;
    }

    private void resize() {
        if (table == null) {
            table = new SimpleEntry[initialCapacity];
        } else {
            if (size >= loadFactor * table.length) {
                SimpleEntry<K, V>[] newTable =
                        new SimpleEntry[table.length << 1];
                for (SimpleEntry<K, V> entry : table) {
                    SimpleEntry<K, V> entryInTable = entry;
                    while (entryInTable != null) {
                        int index = entryInTable.hash & (newTable.length - 1);
                        SimpleEntry<K, V> entr = newTable[index];
                        if (entr == null) {
                            newTable[index] = entryInTable;
                            entryInTable = entryInTable.next;
                            newTable[index].next = null;
                        } else {
                            entr.next = entryInTable;
                            entryInTable = entryInTable.next;
                            entr.next.next = null;
                        }
                    }
                }
                this.table = newTable;
            }
        }

    }

    @Override
    public V remove(K key) {
        int hash = Objects.hashCode(key);
        SimpleEntry<K, V> entry;
        if (table != null) {
            int index = (table.length - 1) & hash;
            entry = table[index];
            if (entry != null) {
                if (hash == entry.hash && Objects.equals(entry.key, key)) {
                    size--;
                    table[index] = entry.next;
                    return entry.value;
                }
                while (entry.next != null) {
                    if (hash == entry.next.hash
                            && Objects.equals(entry.next.key, key)) {
                        size--;
                        V value = entry.next.value;
                        entry.next = entry.next.next;
                        return value;
                    }
                    entry = entry.next;
                }
            }
        }
        return null;
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public Iterator<MyMap.Entry<K, V>> entryIterator() {
        return new EntryIterator();
    }

    private class EntryIterator implements Iterator<MyMap.Entry<K, V>> {
        private SimpleEntry<K, V> current;
        private SimpleEntry<K, V> next;
        private int index = 0;

        EntryIterator() {
            findNext();
        }

        @Override
        public boolean hasNext() {
            return next != null;
        }

        private void findNext() {
            if (table != null) {
                while (index < table.length && table[index] == null) {
                    index++;
                }
                if (index < table.length) {
                    next = table[index++];
                } else {
                    next = null;
                }
            }
        }

        @Override
        public Entry<K, V> next() {
            if (next == null) {
                throw new NoSuchElementException();
            }
            current = next;
            if (next.next != null) {
                next = next.next;
            } else {
                findNext();
            }
            return current;
        }

        @Override
        public void remove() {
            if (current == null) {
                throw new IllegalStateException();
            }
            MyHashMap.this.remove(current.getKey());
            current = null;
        }
    }
}
